/**
 * 
 */
package com.intelap.fabricaciones;

import java.io.Serializable;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.logging.Logger;

import javax.annotation.PostConstruct;
import javax.enterprise.context.Conversation;
import javax.enterprise.context.ConversationScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceUnitUtil;

/** @author ggpollit */
@ConversationScoped
@Named("unidadTrabajo")
public class UnidadTrabajoImpl implements UnidadTrabajo, Serializable {
	@Inject
	private Logger logger;

	private final IdentityHashMap<Entidad<?>, Void> nuevas = new IdentityHashMap<Entidad<?>, Void>();
	private final IdentityHashMap<Entidad<?>, Void> sucias = new IdentityHashMap<Entidad<?>, Void>();
	private final IdentityHashMap<Entidad<?>, Void> quitadas = new IdentityHashMap<Entidad<?>, Void>();
	private boolean terminado = false;

	@Inject
	private EntityManager em;
	@Inject
	private Conversation conversacion;

	private PersistenceUnitUtil puu;

	/** Requerido por CDI para poder hacer que las instancias de esta clase sean proxyables */
	public UnidadTrabajoImpl() {
//		this.em = ProductorRecursos.getManagedBeanEstricto(this, EntityManager.class);
//		this.conversacion = ProductorRecursos.getManagedBeanEstricto(this, Conversation.class);
	}

	@PostConstruct
	private void inicializar() {
		logger.info("Inicializando UnidadTrabajo#" + this.hashCode() + " correspondiente a conversacion " + conversacion.getId());
		this.puu = em.getEntityManagerFactory().getPersistenceUnitUtil();
	}

//	/** Constructor injectable */
//	@Inject
//	public UnidadTrabajo(EntityManager em, Conversation conversacion) {
//		this.em = em;
//		this.conversacion = conversacion;
//	}

	@Override
	public void registrarNueva(Entidad<?> entidad) {
		assert !this.terminado;
		assert entidad != null;
		assert !em.contains(entidad);
//		assert !puu.isLoaded(entidad);

		assert !sucias.containsKey(entidad);
		assert !quitadas.containsKey(entidad);
		this.nuevas.put(entidad, null);
	}

	@Override
	public void registrarSucia(Entidad<?> entidad) {
		assert !this.terminado;
		assert entidad != null;
		if (!em.contains(entidad)) {
			assert !quitadas.containsKey(entidad);
			if (puu.isLoaded(entidad) && !nuevas.containsKey(entidad))
				sucias.put(entidad, null);
		}
	}

	@Override
	public void registrarQuitada(Entidad<?> entidad) {
		assert !this.terminado;
		assert entidad != null;
		if (!em.contains(entidad) && puu.isLoaded(entidad)) {
			if (nuevas.remove(entidad) == null)
				sucias.remove(entidad);
			quitadas.put(entidad, null);
		}
	}

	@Override
	public void registrarLeida(Entidad<?> entidad) {
		assert entidad != null;
		if (!this.terminado && puu.isLoaded(entidad)) {
			// TODO implementar mecanismo optimistic offline lock
		}
	}
	/** Marca esta {@link UnidadTrabajo} como terminada y cierra la conversacion ignorando todos los cambios registrados. */
	@Override
	public void cancelar() {
		this.terminado = true;
		this.conversacion.end();
		logger.info("Conversacion " + conversacion.getId() + " cerrada por UnidadTrabajo#" + this.hashCode());
	}

	/** Hace las siguientes dos cosas en el orden en que se mencionan: <li>Comete todos los cambios realizados en las {@link Entidad}es que sufrieron cambios durante esta unidad de trabajo después de la última llamada a {@link #cometerYContinuar()} <li>Marca esta unidad de trabajo como terminada; */
	@Override
	public void cometerYTerminar() {
		this.cometer(null);
		this.terminado = true;
	}

	/** Comete todos los cambios realizados en las entidades que sufrieron cambios durante esta unidad de trabajo después de la última llamada a {@link #cometerYContinuar()}.
	 * @throws OptimisticLockException si alguna otra transaccion ha modificado alguna de las entidades tratadas (leidas o modificadas) por esta {@link UnidadTrabajo}. De suceder, las {@link Entidad}es con cambios que no llegaron a ser cometidas (debido a la interrupcion abrupta de esta operacion) son recordadas en esta instancia para que quien ataje la excepcion pueda corregir o ignorar la situacion y continuar con el cometido de los cambios llamando nuevamente a esta operacion. No olvidar de llamar a {@link EntityTransaction#setRollbackOnly(boolean)} */
	@Override
	public void cometerYContinuar() {
		Collection<Empalme<?>> empalmes = new LinkedList<>();
		cometer(empalmes);
		for (Empalme<?> empalme : empalmes) {
			empalme.copiarVersion(em);
		}
	}

	private void cometer(Collection<Empalme<?>> empalmes) {
		logger.info("cometer: begin");
		final PersistenceUnitUtil puu = em.getEntityManagerFactory().getPersistenceUnitUtil();

		boolean algunCambioHaSidoCometido;
		int guardianAntiBucleInfinito = 10;
		do {
			if (--guardianAntiBucleInfinito < 0)
				throw new RuntimeException();
			algunCambioHaSidoCometido = false;
			IdentityHashMap<Entidad<?>, Void> copiaNuevas = new IdentityHashMap<>(this.nuevas);
			this.nuevas.clear();
			IdentityHashMap<Entidad<?>, Void> copiaSucias = new IdentityHashMap<>(this.sucias);
			this.sucias.clear();
			IdentityHashMap<Entidad<?>, Void> copiaQuitadas = new IdentityHashMap<>(this.quitadas);
			this.quitadas.clear();
			try {
				// se itera sobre copias para evitar ConcurrentModificationException cuando el acto de cometer los cambios de una entidad provoca cambios en alguna entidad que notifique cambios a esta UnidadTrabajo. 
				for (Iterator<Entidad<?>> iterador = copiaNuevas.keySet().iterator(); iterador.hasNext();) {
					Entidad<?> entidad = iterador.next();
					logger.info("agregando " + entidad);
					em.persist(entidad);
					iterador.remove();
					algunCambioHaSidoCometido = true;
				}
				for (Iterator<Entidad<?>> iterador = copiaSucias.keySet().iterator(); iterador.hasNext();) {
					Entidad<?> entidad = iterador.next();
					logger.info("actualizando " + entidad);
					if (empalmes != null)
						empalmes.add(empalmar(entidad));
					else
						em.merge(entidad);
					iterador.remove();
					algunCambioHaSidoCometido = true;
				}
				for (Iterator<Entidad<?>> iterador = copiaQuitadas.keySet().iterator(); iterador.hasNext();) {
					Entidad<?> entidad = iterador.next();
					logger.info("quitando " + entidad);
					Entidad<?> atada = em.find(entidad.getClass(), puu.getIdentifier(entidad));
					em.remove(atada);
					iterador.remove();
					algunCambioHaSidoCometido = true;
				}
			} catch (Exception e) {
				// se recuerda cuales son las entidades cuyos cambios no fueron cometidos para permitir que quien ataje la excepcion pueda continuar con el cometido de los cambios (que no se llegó a hacer en la llamada anterior) llamando nuevamente a esta operacion.  
				this.nuevas.putAll(copiaNuevas);
				this.sucias.putAll(copiaSucias);
				this.quitadas.putAll(copiaQuitadas);
				throw new RuntimeException("Error durante la ejecucion de la operacion cometerYContinuar : ", e);
			}
		} while (algunCambioHaSidoCometido); // si algun cambio ha sido cometido existe la posibilidad de que el acto de cometer ese cambio haya provocado nuevos cambios en entidades; entonces hay repetir el procedimiento para cometer los nuevos cambios.
		this.em.flush();

		logger.info("cometer: end");
	}

	private <X> Empalme<X> empalmar(Entidad<X> desatada) {
		Entidad<X> atada = em.merge(desatada);
		return new Empalme<X>(atada, desatada);
	}

	private static class Empalme<X> {
		Entidad<X> atada;
		Entidad<X> desatada;
		Empalme(Entidad<X> atada, Entidad<X> desatada) {
			this.atada = atada;
			this.desatada = desatada;
		}
		void copiarVersion(EntityManager em) {
			atada.copiarVersionA(desatada, em);
		}
	}
}
